package org.webdb.webapp.handlers;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Provider
public class ThrowableMapper implements ExceptionMapper<Throwable> {

    private static final Logger logger = LoggerFactory
            .getLogger(ThrowableMapper.class.getSimpleName());

    public static class Stack {

        private String message;
        private String[] stackTrace;
        private String type;

        public Stack(Throwable throwable) {
            this.message = throwable.getMessage();
            this.type = throwable.getClass().getName();
            stackTrace = new String[throwable.getStackTrace().length];
            StackTraceElement el;
            for (int i = 0; i < stackTrace.length; i++) {
                el = throwable.getStackTrace()[i];
                stackTrace[i] = el.getClassName() + '.' + el.getMethodName()
                        + ':' + el.getLineNumber();
            }
        }

        public String getMessage() {
            return message;
        }

        public String[] getStackTrace() {
            return stackTrace;
        }

        public String getType() {
            return type;
        }

    }

    public static class ThrowableEntity {

        private String message;
        private Stack[] stack;
        private String type;

        public ThrowableEntity(Throwable throwable, boolean trace) {
            Throwable cause = findCause(throwable);
            this.type = cause.getClass().getName();
            this.message = cause.getMessage();
            if (trace) {
                LinkedList<Stack> list = new LinkedList<Stack>();
                list.add(new Stack(throwable));
                while (throwable.getCause() != null
                        && throwable.getCause() != throwable) {
                    throwable = throwable.getCause();
                    list.add(new Stack(throwable));
                }

                stack = list.toArray(new Stack[list.size()]);
            }
        }

        public String getMessage() {
            return message;
        }

        public String getType() {
            return type;
        }

        public Stack[] getStack() {
            return stack;
        }

    }

    private static final Comparator<? super Class<?>> CLASS_COMPARATOR = new Comparator<Class<?>>() {

        @SuppressWarnings("rawtypes")
        @Override
        public int compare(Class o1, Class o2) {
            return o1.getName().compareTo(o2.getName());
        }
    };

    private static Throwable findCause(Throwable throwable) {
        while (throwable.getCause() != null
                && throwable.getCause() != throwable)
            throwable = throwable.getCause();

        return throwable;
    }

    private Map<Class<?>, Integer> throwableMap = new TreeMap<Class<?>, Integer>(
            CLASS_COMPARATOR);

    public ThrowableMapper(ServletContext context, String propertiesFile)
            throws IOException, ClassNotFoundException {
        propertiesFile = context
                .getRealPath("/WEB-INF/".concat(propertiesFile));

        Properties properties = new Properties();
        properties.loadFromXML(new FileInputStream(new File(propertiesFile)));
        ClassLoader cl = this.getClass().getClassLoader();
        for (Entry<Object, Object> entry : properties.entrySet()) {
            String[] classes = entry.getValue().toString().split("\\s*,\\s*");
            Integer status = Integer.valueOf(entry.getKey().toString());
            for (String claz : classes) {
                throwableMap.put(cl.loadClass(claz.trim()), status);
            }
        }
    }

    private MediaType determineMediaType(HttpHeaders headers) {
        if (headers.getAcceptableMediaTypes().isEmpty())
            return MediaType.APPLICATION_JSON_TYPE;

        return headers.getAcceptableMediaTypes().get(0);
    }

    @Override
    public Response toResponse(Throwable throwable) {
        HttpHeaders headers = ResteasyProviderFactory
                .getContextData(HttpHeaders.class);

        HttpServletRequest req = ResteasyProviderFactory
                .getContextData(HttpServletRequest.class);

        if (headers != null) {

            Throwable cause = findCause(throwable);
            Integer status = throwableMap.get(cause.getClass());
            Status httpStatus;
            if (status == null)
                httpStatus = Status.INTERNAL_SERVER_ERROR;
            else
                httpStatus = Status.fromStatusCode(status);

            ResponseBuilder builder = Response.status(httpStatus);
            MediaType mediaType = determineMediaType(headers);
            builder.type(mediaType);

            String s = req.getParameter("debug");
            boolean trace = false;
            if (s != null)
                trace = Boolean.parseBoolean(s);

            try {
                builder.entity(new ThrowableEntity(throwable, trace));
            } finally {
                logger.error("Trace", throwable);
            }
            return builder.build();
        }

        return null;
    }
}
